﻿/*
    Copyright (c) 2007 - 2009, Carlos Guzmán Álvarez

    All rights reserved.

    Redistribution and use in source and binary forms, with or without modification, 
    are permitted provided that the following conditions are met:

        * Redistributions of source code must retain the above copyright notice, 
          this list of conditions and the following disclaimer.
        * Redistributions in binary form must reproduce the above copyright notice, 
          this list of conditions and the following disclaimer in the documentation and/or 
          other materials provided with the distribution.
        * Neither the name of the author nor the names of its contributors may be used to endorse or 
          promote products derived from this software without specific prior written permission.

    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
    "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
    LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
    A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
    CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
    EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
    PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
    PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
    LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
    NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
    SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Text;
using System.Threading;
using System.Xml;
using Framework.Net.Xmpp.Serialization;
using Framework.Net.Xmpp.Serialization.Core.ResourceBinding;
using Framework.Net.Xmpp.Serialization.Core.Sasl;
using Framework.Net.Xmpp.Serialization.Core.Streams;
using Framework.Net.Xmpp.Serialization.Core.Tls;
using Framework.Net.Xmpp.Serialization.Extensions;
using Framework.Net.Xmpp.Serialization.Extensions.ServiceDiscovery;
using Framework.Net.Xmpp.Serialization.InstantMessaging.Client;
using Framework.Net.Xmpp.Serialization.InstantMessaging.Client.Presence;
using Framework.Net.Xmpp.Serialization.InstantMessaging.Jabber;
using Framework.Net.Xmpp.Serialization.InstantMessaging.Roster;
using Org.Mentalis.Network.ProxySocket;

namespace Framework.Net.Xmpp.Core
{
	/// <summary>
	/// Represents a connection to a XMPP server
	/// </summary>
	public class XmppConnection : IDisposable
    {
		#region · Consts ·

		const string StreamNamespace	= "jabber:client";
		const string StreamURI			= "http://etherx.jabber.org/streams";
		const string StreamVersion		= "1.0";
		const string StreamFormat		= "<?xml version='1.0' encoding='UTF-8' ?><stream:stream xmlns='{0}' xmlns:stream='{1}' to='{2}' version='{3}'>";
		const string EndStream			= "</stream:stream>";

		#endregion

        #region · Static Methods ·

        /// <summary>
        /// Checks if the given XMPP message instance is an error message
        /// </summary>
        /// <param name="message">The XMPP Message instance</param>
        /// <returns><b>true</b> if it's an error message; otherwise <b>false</b></returns>
        private static bool IsErrorMessage(object message)
        {
            bool isError = false;

            if (message is IQ)
            {
                isError = (((IQ)message).Type == IQType.Error);
            }
            else if (message is StreamError)
            {
                isError = true;
            }
            else if (message is Xmpp.Serialization.Core.Sasl.Failure)
            {
                isError = true;
            }

            return isError;
        }

        #endregion

        #region · Events ·

        /// <summary>
        /// Occurs when the authentication fails
        /// </summary>
        public event EventHandler<XmppAuthenticationFailiureEventArgs> AuthenticationFailiure;

        /// <summary>
		/// Occurs before the connection to the XMPP Server is closed.
		/// </summary>
        public event EventHandler ConnectionClosing;

		/// <summary>
		/// Occurs when the Connection to the XMPP Server is already closed.
		/// </summary>
        public event EventHandler ConnectionClosed;

        /// <summary>
        /// Occurs when a new normal message is received.
        /// </summary>
        public event EventHandler<XmppMessageReceivedEventArgs> NormalMessageReceived;

		/// <summary>
		/// Occurs when a new chat message is received.
		/// </summary>
        public event EventHandler<XmppMessageReceivedEventArgs> ChatMessageReceived;

        /// <summary>
        /// Occurs when a new group chat message is received.
        /// </summary>
        public event EventHandler<XmppMessageReceivedEventArgs> MultiUserChatMessageReceived;

		/// <summary>
		/// Occurs when a new error message is received from the XMPP server.
		/// </summary>
        public event EventHandler<XmppMessageReceivedEventArgs> ErrorMessageReceived;

		/// <summary>
		/// Occurs when a new headline message is received from the XMPP server.
		/// </summary>
        public event EventHandler<XmppMessageReceivedEventArgs> HeadlineMessageReceived;

		/// <summary>
		/// Occurs when a Roster Query message of type Result is received from XMPP Server
		/// </summary>
        public event EventHandler<XmppRosterNotificationEventArgs> RosterMessageReceived;

		/// <summary>
		/// Occurs when a Service Discovery message is received from XMPP Server
		/// </summary>
        public event EventHandler<XmppServiceDiscoveryMessageEventArgs> ServiceDiscoveryMessageReceived;

        /// <summary>
        /// Occurs when a Query (IQ) Error message is received from XMPP Server
        /// </summary>
        public event EventHandler<XmppQueryErrorReceivedEventArgs> QueryErrorReceived;

        /// <summary>
        /// Occurs when a Presence message is received from the XMPP Server
        /// </summary>
        public event EventHandler<XmppPresenceMessageReceivedEventArgs> PresenceMessageReceived;

        /// <summary>
        /// Occurs when an vCard message sis received from the XMPP Server
        /// </summary>
        public event EventHandler<XmppvCardMessageReceiveEventArgs> VCardMessageReceived;

		#endregion

		#region · Internal Events ·

		/// <summary>
		/// Occurs when a new message received from the XMPP server has no handler.
		/// </summary>
        internal event EventHandler<XmppUnhandledMessageEventArgs> UnhandledMessage;

        /// <summary>
        /// Occurs when a sasl failiure occurs.
        /// </summary>
        internal event EventHandler<XmppAuthenticationFailiureEventArgs> AuthenticationError;

		#endregion

		#region · Fields ·
        
		private string					streamIdentifier;
		private ProxySocket 			socket;
        private System.IO.Stream        networkStream;
		private XmppConnectionString	connectionString;
		private XmppConnectionState		state;
		private XmppStreamFeatures		streamFeatures;
		private XmppMemoryStream		inputBuffer;
		private XmppStreamParser		parser;
        private XmppJid                 userId;
        private AutoResetEvent          initializedStreamEvent;
        private AutoResetEvent          streamFeaturesEvent;
        private AutoResetEvent          tlsProceedEvent;
        private AutoResetEvent          bindResourceEvent;
        private AutoResetEvent          iqResponseEvent;
        private AutoResetEvent          endStreamEvent;
        private bool                    isDisposed;
        private object                  syncWrites;

		#endregion

		#region · Properties ·

		/// <summary>
		/// Gets the connection string used on authentication.
		/// </summary>
		public string ConnectionString
		{
			get 
			{
				if (this.connectionString == null)
				{
					return null;
				}

				return this.connectionString.ToString(); 
			}
		}

		/// <summary>
		/// Gets the current state of the connection.
		/// </summary>
		/// <exception cref="InvalidOperationException">
		/// The connection is not open.
		/// </exception>
		public XmppConnectionState State
		{
			get { return this.state; }
		}

		/// <summary>
		/// Gets the Server Name specified in the Connection String.
		/// </summary>
		public string Server
		{
			get 
			{ 
				if (this.connectionString == null)
				{
					return String.Empty;
				}

				return this.connectionString.Server; 
			}
		}

		/// <summary>
		/// Gets the User ID specified in the Connection String.
		/// </summary>
		public XmppJid UserId
		{
			get  { return this.userId; }
		}

        #endregion

        #region · Internal Properties ·

        /// <summary>
        /// Gets the identifier of the XMPP stream
        /// </summary>
        internal string StreamIdentifier
        {
            get { return this.streamIdentifier; }
        }

		/// <summary>
		/// Gets the password specified in the Connection string.
		/// </summary>
		/// <remarks>
		/// Needed for <see cref="XmppAuthenticator"/> implementations.
		/// </remarks>
		internal string UserPassword
		{
			get 
			{
				if (this.connectionString == null)
				{
					return String.Empty;
				}

                return this.connectionString.UserPassword; 
			}
		}

		#endregion

        #region · Constructors ·

        /// <summary>
		/// Initializes a new instance of the <b>XmppConnection</b> class.
		/// </summary>
        public XmppConnection()
		{
            this.syncWrites = new object();
		}

		#endregion
        
        #region · Finalizer ·

        /// <summary>
        /// Releases unmanaged resources and performs other cleanup operations before the
        /// <see cref="T:Framework.Net.Xmpp.Core.XmppConnection"/> is reclaimed by garbage collection.
        /// </summary>
        ~XmppConnection()
        {
            // Do not re-create Dispose clean-up code here.
            // Calling Dispose(false) is optimal in terms of
            // readability and maintainability.
            this.Dispose(false);
        }

        #endregion

        #region · IDisposable Methods ·

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            this.Dispose(true);

            // This object will be cleaned up by the Dispose method.
            // Therefore, you should call GC.SupressFinalize to
            // take this object off the finalization queue 
            // and prevent finalization code for this object
            // from executing a second time.
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Disposes the specified disposing.
        /// </summary>
        /// <param name="disposing">if set to <c>true</c> [disposing].</param>
        protected virtual void Dispose(bool disposing)
        {
            if (!this.isDisposed)
            {
                if (disposing)
                {
                    // Release managed resources here
                    this.Close();
                }

                // Call the appropriate methods to clean up 
                // unmanaged resources here.
                // If disposing is false, 
                // only the following code is executed.
                
                this.syncWrites = null;
            }

            this.isDisposed = true;
        }

        #endregion

        #region · Methods ·

        /// <summary>
        /// Opens the connection
        /// </summary>
        /// <param name="connectionString">The connection string used for authentication.</param>
        public void Open(string connectionString)
		{
			if (this.state == XmppConnectionState.Open)
			{
				throw new XmppException("Connection must be closed first.");
			}

			try
			{
                // Initialization
                this.Initialize();

                // Build the connection string
				this.connectionString   = new XmppConnectionString(connectionString);
                this.userId             = new XmppJid(this.connectionString.UserId);

				// Connect to the server
				this.Connect();

                if (this.connectionString.PortNumber == 443 ||
                    this.connectionString.PortNumber == 5223)
                {
                    this.OpenSecureStream();
                }

                // Begin Receiving Data
                this.BeginReceiveData();

                // Initialize XMPP Stream
                this.InitializeStream();

                // Wait until we receive the Stream features
                this.WaitForStreamFeatures();

                if (this.connectionString.PortNumber != 443 &&
                    this.connectionString.PortNumber != 5223)
                {
                    if (this.SupportsFeature(XmppStreamFeatures.SecureConnection))
                    {
                        this.OpenSecureConnection();
                    }
                }

				// Perform authentication
				bool authenticationDone = this.Authenticate();

                if (authenticationDone)
                {
                    // Resource Binding.
                    this.BindResource();

                    // Open the session
                    this.OpenSession();

                    // Update state
                    this.state = XmppConnectionState.Open;
                }
			}
			catch (Exception ex)
			{
                this.AuthenticationError(this, new XmppAuthenticationFailiureEventArgs(ex.ToString()));
				this.Close();
			}
		}

		/// <summary>
		/// Closes the connection
		/// </summary>
		public void Close()
		{
			if (this.state != XmppConnectionState.Closed)
			{
				if (this.ConnectionClosing != null)
				{
					this.ConnectionClosing(this, new EventArgs());
				}

                try
                {
                    this.Send(EndStream);

                    this.endStreamEvent.WaitOne();
                }
                catch
                {
                }
                finally
                {
                    this.inputBuffer.Dispose();
					this.inputBuffer.Dispose();
                    this.parser.Dispose();

                    this.initializedStreamEvent.Set();
                    this.streamFeaturesEvent.Set();
                    this.tlsProceedEvent.Set();
                    this.bindResourceEvent.Set();
                    this.iqResponseEvent.Set();
                    this.endStreamEvent.Set();

                    this.initializedStreamEvent.Close();
                    this.streamFeaturesEvent.Close();
                    this.tlsProceedEvent.Close();
                    this.bindResourceEvent.Close();
                    this.iqResponseEvent.Close();
                    this.endStreamEvent.Close();

                    if (this.networkStream != null)
                    {
                        this.networkStream.Dispose();
                        this.networkStream = null;
                    }
                    if (this.socket != null)
                    {
                        this.socket.Close();
                        this.socket = null;
                    }

                    this.streamFeatures         = this.streamFeatures & (~this.streamFeatures);
                    this.state                  = XmppConnectionState.Closed;
                    this.streamIdentifier       = null;
					this.connectionString		= null;
					this.inputBuffer			= null;
					this.parser					= null;				
					this.initializedStreamEvent = null;
                    this.streamFeaturesEvent    = null;
                    this.tlsProceedEvent        = null;
                    this.bindResourceEvent      = null;
                    this.iqResponseEvent        = null;
                    this.endStreamEvent         = null;
                }

                if (this.ConnectionClosed != null)
                {
                    this.ConnectionClosed(this, new EventArgs());
                }
			}
		}

		/// <summary>
		/// Sends a new message.
		/// </summary>
		/// <param elementname="message">The message to be sent</param>
        public void Send(object message)
		{
            this.Send(XmppSerializer.Serialize(message));
		}

		#endregion

		#region · Stream Initialization ·

        internal void WaitForStreamFeatures()
        {
            this.streamFeaturesEvent.WaitOne();
        }

        /// <summary>
        /// Initializes the XMPP stream.
        /// </summary>
        internal void InitializeStream()
        {
            // Reset actual Stream ID
            this.streamIdentifier = null;

            // Serialization can't be used in this case
            string xml = String.Format(
                StreamFormat,
                StreamNamespace,
                StreamURI,
                this.UserId.DomainName,
                StreamVersion);

            this.Send(xml);

            // Wait until we have the stream:stream element readed
            this.initializedStreamEvent.WaitOne();
        }

        #endregion

        #region · Protected methods ·

        /// <summary>
		/// Opens the connection to the XMPP server.
		/// </summary>
		protected virtual void Connect()
		{
			try
			{
                IPAddress   hostadd         = Dns.GetHostEntry(this.connectionString.Server).AddressList[0];
                IPEndPoint  hostEndPoint    = new IPEndPoint(hostadd, this.connectionString.PortNumber);

                this.socket = new ProxySocket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

                if (this.connectionString.UseProxy)
                {
                    IPAddress   proxyadd        = Dns.GetHostEntry(this.connectionString.ProxyServer).AddressList[0];
                    IPEndPoint  proxyEndPoint   = new IPEndPoint(hostadd, this.connectionString.ProxyPortNumber);

                    switch (this.connectionString.ProxyType)
                    {
                        case "SOCKS4":
                            this.socket.ProxyType = ProxyTypes.Socks4;
                            break;

                        case "SOCKS5":
                            this.socket.ProxyType = ProxyTypes.Socks5;
                            break;

                        default:
                            this.socket.ProxyType = ProxyTypes.None;
                            break;
                    }

                    this.socket.ProxyEndPoint   = proxyEndPoint;
                    this.socket.ProxyUser       = this.connectionString.ProxyUserName;
                    if (!String.IsNullOrEmpty(this.connectionString.ProxyPassword))
                    {
                        this.socket.ProxyPass = this.connectionString.ProxyPassword;
                    }
                }                

				// Disables the Nagle algorithm for send coalescing.
                this.socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, 1);

				// Make the socket to connect to the Server
                this.socket.Connect(hostEndPoint);

				// Create the Stream Instance
                this.networkStream = new NetworkStream(this.socket, false);
			}
			catch (Exception ex)
			{
				throw new XmppException(String.Format("Unable to connect to XMPP server {0}", this.connectionString.Server), ex);
			}
		}

		#endregion

		#region · Private Read Methods ·

        /// <summary>
        /// Startds async readings on the socket connected to the server
        /// </summary>
        private void BeginReceiveData() 
		{
            StateObject     state           = new StateObject(this.networkStream);
            AsyncCallback   asyncCallback   = new AsyncCallback(ReceiveCallback);

            // Begin receiving the data from the remote device.
            IAsyncResult ar = state.WorkStream.BeginRead(state.Buffer, 0, state.Buffer.Length, asyncCallback, state);

            if (ar.CompletedSynchronously)
            {
                this.ProcessAsyncRead(ar);
            }
		}

        /// <summary>
        /// Async read callback
        /// </summary>
        /// <param name="ar"></param>
		private void ReceiveCallback(IAsyncResult ar)
		{
            if (!ar.CompletedSynchronously)
            {
                this.ProcessAsyncRead(ar);
            }
        }

        /// <summary>
        /// Assync read processing.
        /// </summary>
        /// <param name="ar"></param>
        private void ProcessAsyncRead(IAsyncResult ar)
        {
            // Retrieve the state object and the client socket 
            // from the asynchronous state object.
            StateObject             state       = (StateObject)ar.AsyncState;
            List<AutoResetEvent>    resetEvents = null;

            if (state.WorkStream != null && state.WorkStream.CanRead)
            {
                // Read data from the remote device.
                int bytesRead = state.WorkStream.EndRead(ar);

                if (bytesRead > 0)
                {
                    Monitor.Enter(this.inputBuffer);

                    long currentPosition = this.inputBuffer.Position;

                    this.inputBuffer.Seek(0, SeekOrigin.End);
                    this.inputBuffer.Write(state.Buffer, 0, bytesRead);
                    this.inputBuffer.Flush();
                    
                    this.inputBuffer.Seek(currentPosition, SeekOrigin.Begin);

                    Debug.Write(Encoding.UTF8.GetString(state.Buffer, 0, bytesRead));
                    Debug.Flush();

                    Monitor.Exit(this.inputBuffer);

                    resetEvents = this.ProcessPendingMessages();

                    if (resetEvents != null && resetEvents.Count > 0)
                    {
                        foreach (AutoResetEvent resetEvent in resetEvents)
                        {
                            resetEvent.Set();
                        }
                    }
                    else
                    {
                        this.BeginReceiveData();
                    }
                }
                else
                {
                    // Signal the end of the stream
                    this.endStreamEvent.Set();
                }
            }            
        }

		#endregion

		#region · Private Send Methods ·

        /// <summary>
        /// Sends an XMPP message string to the XMPP Server
        /// </summary>
        /// <param name="value"></param>
		private void Send(string value)
		{
			this.Send(Encoding.UTF8.GetBytes(value));
		}

        /// <summary>
        /// Sends an XMPP message buffer to the XMPP Server
        /// </summary>
        private void Send(byte[] buffer)
		{
            lock (this.syncWrites)
            {
                Debug.Write(Encoding.UTF8.GetString(buffer, 0, buffer.Length));
                Debug.Flush();

                this.networkStream.Write(buffer, 0, buffer.Length);
                this.networkStream.Flush();
            }
        }

        #endregion

		#region · Message Handling ·

        /// <summary>
        /// Process all pending XMPP messages
        /// </summary>
        /// <returns></returns>
        private List<AutoResetEvent> ProcessPendingMessages()
		{
            List<AutoResetEvent> resetEvents = new List<AutoResetEvent>();

            while (!this.parser.EOF)
            {
                AutoResetEvent resetEvent = this.ProcessXmppMessage(this.parser.ReadNextNode());

                if (resetEvent != null)
                {
                    resetEvents.Add(resetEvent);
                }
            }

            return resetEvents;
		}

        /// <summary>
        /// Procesa an arbitrary XMPP Message
        /// </summary>
        /// <param name="node"></param>
        /// <returns></returns>
        private AutoResetEvent ProcessXmppMessage(XmppStreamElement node)
        {
            if (node != null)
            {
                if (node.OpensXmppStream)
                {
                    this.ProcessStreamInitialization(node);
                }
                else if (node.ClosesXmppStream)
                {
                    return this.endStreamEvent;
                }
                else
                {
                    object message = XmppSerializer.Deserialize(node.Name, node.ToString());

                    if (message != null)
                    {
                        if (XmppConnection.IsErrorMessage(message))
                        {
                            return this.ProcessErrorMessage(message);
                        }
                        else if (message is IQ)
                        {
                            this.ProcessQueryMessage(message as IQ);
                        }
                        else if (message is Presence)
                        {
                            this.ProcessPresenceMessage(message as Presence);
                        }
                        else if (message is Message)
                        {
                            this.ProcessMessage(message as Message);
                        }
                        else if (message is StreamFeatures)
                        {
                            this.ProcessStreamFeatures(message as StreamFeatures);
                        }
                        else if (message is Proceed)
                        {
                            return this.tlsProceedEvent;
                        }
                        else if (this.UnhandledMessage != null)
                        {
                            this.UnhandledMessage(this, new XmppUnhandledMessageEventArgs(message));
                        }
                    }
                    else
                    {
                        string a = "";
                    }
                }            
            }

            return null;
        }

        /// <summary>
        /// Process the Stream initialization XMPP message
        /// </summary>
        /// <param name="node"></param>
		private void ProcessStreamInitialization(XmppStreamElement node)
		{
			XmlTextReader reader = new XmlTextReader(node.Node, XmlNodeType.Element, null);
            reader.Read();

            this.streamIdentifier = reader["id"];

            if (reader.NamespaceURI != XmppCodes.XmppNamespaceURI)
			{
				throw new InvalidOperationException("Incorrect namespace URI in stream initialization");
			}

			this.initializedStreamEvent.Set();
		}

        /// <summary>
        /// Process an XMPP Error message
        /// </summary>
        /// <param name="message"></param>
        private AutoResetEvent ProcessErrorMessage(object message)
        {
            if (message is IQ)
            {
                if (this.QueryErrorReceived != null)
                {
                    this.QueryErrorReceived(this, new XmppQueryErrorReceivedEventArgs(message as IQ));
                }
            }
            else if (message is StreamError)
            {
                this.ThrowStreamErrorException((StreamError)message);
            }
            else if (message is Failure)
            {
                if (this.AuthenticationError != null)
                {
                    string errorMessage = String.Format("SASL Authentication Failed ({0})", ((Failure)message).FailiureType);

                    this.AuthenticationError(this, new XmppAuthenticationFailiureEventArgs(errorMessage));
                }
            }

            return null;
        }

        /// <summary>
        /// Process an Stream Features XMPP message
        /// </summary>
        /// <param name="features"></param>
		private void ProcessStreamFeatures(StreamFeatures features)
		{
			if (features.Mechanisms != null && features.Mechanisms.SaslMechanisms.Count > 0)
			{
				foreach (string mechanism in features.Mechanisms.SaslMechanisms)
				{
					switch (mechanism)
					{
						case XmppCodes.SaslDigestMD5Mechanism:
							this.streamFeatures |= XmppStreamFeatures.SaslDigestMD5;
							break;

						case XmppCodes.SaslPlainMechanism:
							this.streamFeatures |= XmppStreamFeatures.SaslPlain;
							break;

                        case XmppCodes.SaslXGoogleTokenMechanism:
                            this.streamFeatures |= XmppStreamFeatures.XGoogleToken;
                            break;
                    }
				}
			}

			if (features.Bind != null)
			{
				this.streamFeatures |= XmppStreamFeatures.ResourceBinding;
			}

			if (features.SessionSpecified)
			{
				this.streamFeatures |= XmppStreamFeatures.Sessions;
			}

			if (features.Items.Count > 0)
			{
				foreach (object item in features.Items)
				{
					if (item is Framework.Net.Xmpp.Serialization.InstantMessaging.RegisterIQ)
					{
						this.streamFeatures |= XmppStreamFeatures.InBandRegistration;
					}
				}
			}

			this.streamFeaturesEvent.Set();
		}

        /// <summary>
        /// Process an XMPP Message
        /// </summary>
        /// <param name="message"></param>
        private void ProcessMessage(Message message)
        {
            XmppMessageReceivedEventArgs args = new XmppMessageReceivedEventArgs(message);

            switch (message.Type)
            {
                case MessageType.Normal:
                    if (this.NormalMessageReceived != null)
                    {
                        this.NormalMessageReceived(this, args);
                    }
                    break;

                case MessageType.Chat:
                    if (this.ChatMessageReceived != null)
                    {
                        this.ChatMessageReceived(this, args);
                    }
                    break;

                case MessageType.Error:
                    if (this.ErrorMessageReceived != null)
                    {
                        this.ErrorMessageReceived(this, args);
                    }
                    break;

                case MessageType.GroupChat:
                    if (this.MultiUserChatMessageReceived != null)
                    {
                        this.MultiUserChatMessageReceived(this, args);
                    }
                    break;

                case MessageType.Headline:
                    if (this.HeadlineMessageReceived != null)
                    {
                        this.HeadlineMessageReceived(this, args);
                    }
                    break;
            }
        }

        /// <summary>
        /// Process an XMPP IQ message
        /// </summary>
        /// <param name="iq"></param>
        private void ProcessQueryMessage(IQ iq)
        {
            if (iq.Items.Count > 0)
            {
                object item = iq.Items[0];

                if (iq.Type != IQType.Error)
                {
                    if (item is Bind)
                    {
                        this.userId = new XmppJid(((Bind)item).JID);

                        this.bindResourceEvent.Set();
                    }
                    else if (item is RosterQuery)
                    {
                        if (this.RosterMessageReceived != null)
                        {
                            this.RosterMessageReceived(this, new XmppRosterNotificationEventArgs((RosterQuery)iq.Items[0]));
                        }
                    }
                    else if (item is VCard)
                    {
                        if (this.VCardMessageReceived != null)
                        {
                            this.VCardMessageReceived(this, new XmppvCardMessageReceiveEventArgs(iq));
                        }
                    }
                    else if (item is JabberAuthQuery)
                    {
                        if (this.UnhandledMessage != null)
                        {
                            this.UnhandledMessage(this, new XmppUnhandledMessageEventArgs(iq));
                        }
                    }
                }

                if (item is ServiceQuery || item is ServiceItemQuery)
                {
                    if (this.ServiceDiscoveryMessageReceived != null)
                    {
                        this.ServiceDiscoveryMessageReceived(this, new XmppServiceDiscoveryMessageEventArgs(iq));
                    }
                }
            }

            this.iqResponseEvent.Set();
        }

        /// <summary>
        /// Processes the presence message.
        /// </summary>
        /// <param name="presence">The presence.</param>
        /// <returns></returns>
        private bool ProcessPresenceMessage(Presence presence)
        {
            if (this.PresenceMessageReceived != null)
            {
                this.PresenceMessageReceived(this, new XmppPresenceMessageReceivedEventArgs(presence));
            }

            return true;
        }

		#endregion

        #region · Secure Connection Methods ·

        /// <summary>
        /// Opens a secure connection against the XMPP server using TLS
        /// </summary>
        private void OpenSecureConnection()
        {
            // Send Start TLS message
            StartTls tlsMsg = new StartTls();
            this.Send(tlsMsg);

            this.tlsProceedEvent.WaitOne();

            this.OpenSecureStream();

            // Reset actual Stream ID
            this.streamIdentifier = null;

            // Clear the Input Buffer
            this.inputBuffer.Clear();

            // Re-Start Async Reads
            this.BeginReceiveData();

            // Re-Initialize XMPP Stream
            this.InitializeStream();

            // Wait until we receive the Stream features
            this.WaitForStreamFeatures();
        }

        private void OpenSecureStream()
        {
            // Initialize the Ssl Stream
            this.networkStream = new SslStream(
                new NetworkStream(this.socket, false),
                false,
                new RemoteCertificateValidationCallback(RemoteCertificateValidation));

            // Perform SSL Authentication
            ((SslStream)this.networkStream).AuthenticateAsClient(
                this.connectionString.Server,
                null,
                SslProtocols.Tls,
                false);
        }

        #endregion

		#region · Authentication ·

		private bool Authenticate()
		{
			XmppAuthenticator   authenticator   = null;
            bool                result          = false;
			
			try
			{
				authenticator = this.CreateAuthenticator();

				authenticator.Authenticate();

                if (authenticator.AuthenticationFailed)
                {
                    if (this.AuthenticationFailiure != null)
                    {
                        this.AuthenticationFailiure(this, new XmppAuthenticationFailiureEventArgs(authenticator.AuthenticationError));
                    }
                }

                result = !authenticator.AuthenticationFailed;
			}
			catch 
			{
				throw;
			}
			finally
			{
				if (authenticator != null)
				{
					authenticator.Dispose();
					authenticator = null;
				}
			}

            return result;
		}

		#endregion

        #region · SSL/TLS Callbacks ·

		/// <summary>
		/// Validation of the remote X509 Certificate ( on SSL/TLS connections only )
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="certificate"></param>
		/// <param name="chain"></param>
		/// <param name="sslPolicyErrors"></param>
		/// <returns></returns>
        private bool RemoteCertificateValidation(object sender,
            System.Security.Cryptography.X509Certificates.X509Certificate certificate,
            System.Security.Cryptography.X509Certificates.X509Chain chain,
            SslPolicyErrors sslPolicyErrors)
        {
#warning Give the option to make this handled by the application using the library
            return true;
        }

        #endregion

        #region · Feature negotiation ·

        private void BindResource()
        {
			if (this.SupportsFeature(XmppStreamFeatures.ResourceBinding))
			{
				Bind bind		= new Bind();
				bind.Resource	= this.UserId.ResourceName;

				IQ iq   = new IQ();
				iq.Type = IQType.Set;
				iq.ID   = XmppIdentifierGenerator.Generate();

				iq.Items.Add(bind);

				this.Send(iq);

				this.bindResourceEvent.WaitOne();
			}
        }

        private void OpenSession()
        {
			if (this.SupportsFeature(XmppStreamFeatures.Sessions))
			{
				IQ iq = new IQ();
            
				iq.Type = IQType.Set;
				iq.To   = this.connectionString.Server;
				iq.ID   = XmppIdentifierGenerator.Generate();

				iq.Items.Add(new Session());

				this.Send(iq);
			}
        }

        #endregion

		#region · Private Methods ·

        /// <summary>
        /// Initializes the connection instance
        /// </summary>
        private void Initialize()
        {
            this.state                  = XmppConnectionState.Opening;
            this.inputBuffer            = new XmppMemoryStream();
            this.parser                 = new XmppStreamParser(this.inputBuffer);
            this.initializedStreamEvent = new AutoResetEvent(false);
            this.streamFeaturesEvent    = new AutoResetEvent(false);
            this.tlsProceedEvent        = new AutoResetEvent(false);
            this.bindResourceEvent      = new AutoResetEvent(false);
            this.iqResponseEvent        = new AutoResetEvent(false);
            this.endStreamEvent         = new AutoResetEvent(false);
        }

        /// <summary>
        /// Creates an instance of the <see cref="XmppAuthenticator"/> used by the connection.
        /// </summary>
        /// <returns></returns>
		private XmppAuthenticator CreateAuthenticator()
		{
            XmppAuthenticator authenticator = new XmppJabberAuthenticator(this);

			if (this.SupportsFeature(XmppStreamFeatures.SaslDigestMD5))
			{
                authenticator = new XmppSaslDigestAuthenticator(this);
			}
            else if (this.SupportsFeature(XmppStreamFeatures.XGoogleToken))
            {
                authenticator = new XmppSaslXGoogleTokenAuthenticator(this);
            }
			else if (this.SupportsFeature(XmppStreamFeatures.SaslPlain))
			{
                authenticator = new XmppSaslPlainAuthenticator(this);
			}

            return authenticator;
		}

		/// <summary>
		/// Checks if a speficic stream feature is supported by the XMPP server.
		/// </summary>
		/// <param elementname="feature">Feature to check.</param>
		/// <returns><b>true</b> if the feature is supported by the server; or <b>false</b> if not</returns>
		private bool SupportsFeature(XmppStreamFeatures feature)
		{
			return ((this.streamFeatures & feature) == feature);
		}

        private void ThrowStreamErrorException(StreamError message)
        {
            StringBuilder exceptionMessage = new StringBuilder();

            if (message.BadFormat != null)
            {
                exceptionMessage.Append("bad-format");
            }            
            else if (message.BadNamespacePrefix != null)
            {
                exceptionMessage.Append("bad-namespace-prefix");
            }
            else if (message.Conflict != null)
            {
                exceptionMessage.Append("conflict");
            }
            else if (message.ConnectionTimeout != null)
            {
                exceptionMessage.Append("connection-timeout");
            }
            else if (message.HostGone != null)
            {
                exceptionMessage.Append("host-gone");
            }
            else if (message.HostUnknown != null)
            {
                exceptionMessage.Append("host-unknown");
            }
            else if (message.ImproperAddressing != null)
            {
                exceptionMessage.Append("improper-addressing");
            }
            else if (message.InternalServerError != null)
            {
                exceptionMessage.Append("internal-server-error");
            }
            else if (message.InvalidFrom != null)
            {
                exceptionMessage.Append("invalid-from");
            }
            else if (message.InvalidID != null)
            {
                exceptionMessage.Append("invalid-id");
            }
            else if (message.InvalidNamespace != null)
            {
                exceptionMessage.Append("invalid-namespace");
            }
            else if (message.InvalidXml != null)
            {
                exceptionMessage.Append("invalid-xml");
            }
            else if (message.NotAuthorized != null)
            {
                exceptionMessage.Append("not-authorized");
            }
            else if (message.PolicyViolation != null)
            {
                exceptionMessage.Append("policy-violation");
            }
            else if (message.RemoteConnectionFailed != null)
            {
                exceptionMessage.Append("remote-connection-failed");
            }
            else if (message.ResourceConstraint != null)
            {
                exceptionMessage.Append("resource-constraint");
            }
            else if (message.RestrictedXml != null)
            {
                exceptionMessage.Append("restricted-xml");
            }
            else if (message.SeeOtherHost != null)
            {
                exceptionMessage.Append("see-other-host");
            }
            else if (message.SystemShutdown != null)
            {
                exceptionMessage.Append("system-shutdown");
            }
            else if (message.UndefinedCondition != null)
            {
                exceptionMessage.Append("undefined-condition");
            }
            else if (message.UnsupportedEncoding != null)
            {
                exceptionMessage.Append("unsupported-encoding");
            }
            else if (message.UnsupportedStanzaType != null)
            {
                exceptionMessage.Append("unsupported-stanza-type");
            }
            else if (message.UnsupportedVersion != null)
            {
                exceptionMessage.Append("unsupported-version");
            }
            else if (message.XmlNotWellFormed != null)
            {
                exceptionMessage.Append("xml-not-well-formed");
            }

            if (message.Text != null && message.Text.Value != null)
            {
                exceptionMessage.Append(Environment.NewLine);
                exceptionMessage.Append("message.Text.Value");
            }

            throw new XmppException(exceptionMessage.ToString());
        }

		#endregion
    }
}
